Skip to content

Conversation

@matouskozak
Copy link
Member

@matouskozak matouskozak commented Jan 16, 2026

  • Extends existing support for user breakpoint to regular IDE breakpoints
  • Add support for adding breakpoints when program is stopped
  • Insert IL offset 0 poiting to IR 0 to allow setting breakpoints at method entry (opening '{')

TODO:

  • Single-stepping for interpreter

@matouskozak matouskozak self-assigned this Jan 16, 2026
@matouskozak matouskozak added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) area-Diagnostics-coreclr labels Jan 16, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@matouskozak matouskozak force-pushed the interpreter-breakpoints branch from ed5ce4c to dce1adc Compare January 16, 2026 08:52
- New InterpreterWalker class decodes bytecode control flow for stepping
- Update TrapStep to use InterpreterWalker for interpreted code
- Add per-thread TSNC_InterpreterSingleStep flag for step tracking
- ApplyPatch now uses INTOP_SINGLESTEP vs INTOP_BREAKPOINT based on flag
- Handle INTOP_SINGLESTEP in interpreter execution loop
Copilot AI review requested due to automatic review settings January 30, 2026 10:59
- needed for step-in support in virtual calls
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This work-in-progress pull request adds support for managed debugger breakpoints in the CoreCLR interpreter. The changes extend the existing user breakpoint support (e.g., Debugger.Break()) to support IDE breakpoints and enable setting breakpoints when the program is stopped.

Changes:

  • Adds interpreter single-step thread state flag and supporting methods
  • Introduces new INTOP_SINGLESTEP opcode for step-over operations
  • Implements InterpreterWalker to analyze interpreter bytecode for debugger stepping
  • Modifies breakpoint execution logic to distinguish between IDE breakpoints and step-out breakpoints
  • Enables JIT completion notifications for interpreter code
  • Pre-inserts IL offset 0 entry in the IL-to-native map to support method entry breakpoints

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/coreclr/vm/threads.h Adds TSNC_InterpreterSingleStep thread state flag and related methods
src/coreclr/vm/jitinterface.cpp Removes interpreter code exclusion from JITComplete notifications
src/coreclr/vm/interpexec.cpp Implements breakpoint and single-step handling with opcode replacement
src/coreclr/vm/codeman.h Adds IsInterpretedCode() helper method
src/coreclr/interpreter/intops.h Adds helper functions to classify interpreter opcodes
src/coreclr/interpreter/inc/intops.def Defines INTOP_SINGLESTEP opcode
src/coreclr/interpreter/compiler.cpp Pre-inserts IL offset 0 mapping for method entry breakpoints
src/coreclr/debug/ee/interpreterwalker.h Declares InterpreterWalker class for bytecode analysis
src/coreclr/debug/ee/interpreterwalker.cpp Implements bytecode walker for debugger stepping operations
src/coreclr/debug/ee/functioninfo.cpp Uses GetInterpreterCodeFromInterpreterPrecodeIfPresent for code address
src/coreclr/debug/ee/executioncontrol.h Defines BreakpointInfo structure and GetBreakpointInfo method
src/coreclr/debug/ee/executioncontrol.cpp Implements INTOP_SINGLESTEP patch support and breakpoint info retrieval
src/coreclr/debug/ee/controller.h Includes interpreterwalker.h header
src/coreclr/debug/ee/controller.cpp Implements TrapStep for interpreter using InterpreterWalker
src/coreclr/debug/ee/CMakeLists.txt Adds interpreterwalker source files to build

Copilot AI review requested due to automatic review settings January 30, 2026 11:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.

Comment on lines 17 to +24
#ifdef FEATURE_INTERPRETER

// Result of GetBreakpointInfo - combines opcode and step-out flag
struct BreakpointInfo
{
InterpOpcode originalOpcode;
bool isStepOut;
};
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header file uses InterpOpcode type but doesn't include the header that defines it. While this works when included from files that already have the interpreter headers included (like interpexec.cpp), it violates header self-containment principles and could cause compilation errors if this header is included elsewhere. Add #include "../../interpreter/inc/intopsshared.h" after line 15 or before the BreakpointInfo struct definition to ensure the header is self-contained.

Copilot uses AI. Check for mistakes.
Comment on lines +5681 to +5692
// For FEATURE_INTERPRETER, the SP can also point to interpreter stack memory which is outside native stack bounds.
// Skip validation in these cases as the interpreter uses heap-allocated frame structures.
// TODO: Figure this out without disabling the check entirely.
#if !defined(TARGET_WASM) && !defined(FEATURE_INTERPRETER)
if (pRD->SP && pRD->_pThread)
{
#ifndef NO_FIXED_STACK_LIMIT
_ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) >= pRD->_pThread->GetCachedStackLimit());
#endif // NO_FIXED_STACK_LIMIT
_ASSERTE(pRD->_pThread->IsExecutingOnAltStack() || PTR_VOID(pRD->SP) < pRD->_pThread->GetCachedStackBase());
}
#endif // !TARGET_WASM
#endif // !TARGET_WASM && !FEATURE_INTERPRETER
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling the SP validation check for all FEATURE_INTERPRETER builds is overly broad and reduces debug coverage. The check should only be skipped when actually in interpreter frames, not for all code when the interpreter feature is enabled. Consider checking if the current frame is an interpreter frame (e.g., using EECodeInfo::IsInterpretedCode on the current IP) before skipping the validation, similar to how the TARGET_WASM case is handled. This would preserve the debug check for JIT code while still allowing interpreter frames to work correctly.

Copilot uses AI. Check for mistakes.
{
// Indirect call (CALLVIRT, CALLI, CALLDELEGATE) - cannot determine target statically
// Use JMC backstop to catch method entry
// TODO: Could we do better? Why we can't use StubManagers to trace indirect calls?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can StubManagers if there is one that recognizes the code pattern being used to make the indirect call. If the indirect call doesn't need too many instructions to reach the destination you could also enable single-stepping and get there that way.

const int32_t* skipIP = interpWalker.GetSkipIP();
if (skipIP != NULL)
{
EnableSingleStep();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we'd enable single step here? The next line appears to set a breakpoint on the target already.

const int32_t* nextIP = interpWalker.GetNextIP();
if (nextIP != NULL)
{
EnableSingleStep();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both breakpoint and single-stepping is unnecessary. If the breakpoint is where we expect execution to be after one instruction single step might be simpler.

int32_t caseCount = interpWalker.GetSwitchCaseCount();
LOG((LF_CORDB,LL_INFO10000,"DS::TS: INTOP_SWITCH with %d cases\n", caseCount));

EnableSingleStep();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its unnecessary to have both single-stepping and breakpoints on all the cases. If all the breakpoints are 1 instruction in the future perhaps its easier to rely on the single step rather than set all the breakpoints?

const int32_t* nextIP = interpWalker.GetNextIP(); // branch target
const int32_t* skipIP = interpWalker.GetSkipIP(); // fallthrough

EnableSingleStep();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

const int32_t* skipIP = interpWalker.GetSkipIP();
if (skipIP != NULL)
{
EnableSingleStep();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing

(const BYTE*)(GetControlPC(&info->m_activeFrame.registers))));
}

#ifdef FEATURE_INTERPRETER
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as a code factoring nit this function is already very long. I think it would be a little easier to follow if we factored some of this out into separate functions. Something like:

if(isInterpretter)
{
   return TrapInterpretterCodeStep(...)
}
else
{
    return TrapNativeCodeStep(...);
}

*(uint32_t*)patch->address = INTOP_BREAKPOINT;
// Check if there is already a breakpoint patch at this address
uint32_t currentOpcode = *(uint32_t*)patch->address;
if (currentOpcode == INTOP_BREAKPOINT || currentOpcode == INTOP_SINGLESTEP)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems suspicious. the debugger could do something like this:
EnableSingleStepping()
ApplyPatch(nextAddress)
DisableSingleStepping()

The debugger would expect the breakpoint is still set at the end of that sequence but this code appears to avoid setting a breakpoint when single stepping is also enabled.

My guess is you either want one special opcode with state tracking on the side to describe what happens when you hit it, or you want three opcodes representing BREAKPOINT, SINGLESTEP, BREAKPOINT_AND_SINGLESTEP.

Also there is a comment lower down with broader questions about the single stepping abstraction. If we decide to go with the debugger having an interpretter single step emulator then I'd expect INTOP_SINGLESTEP goes away.

int32_t opcode = *ip;

// If this is a breakpoint or single-step patch, get the original opcode from the patch table
if (opcode == INTOP_BREAKPOINT || opcode == INTOP_SINGLESTEP)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't expect to see entries in the debugger patch table when doing single stepping. That suggests the interpretter isn't really providing the normal single-stepping abstraction because the debugger doesn't track any patches when doing hardware single stepping.

I think we'd want one of:

  1. a complete single step abstraction implemented at the interpreter layer with no dependencies on the debugger patch table
  2. no single step at the interpretter layer and an InterpretterSingleStepEmulator at the debugger layer that implements single steps using interpretter breakpoints and predicting the next instruction as primitives.

Right now the implementation seems like a mixture of both, but closer to (2) than to (1).

return true;
}

BreakpointInfo InterpreterExecutionControl::GetBreakpointInfo(const void* address) const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll come back to this since I need to run for the moment, but I'm suspicious about the abstraction here. I think of these ExecutionControl APIs as being a layer below the debugger, something the debugger depends on. I did not expect them to be using the debugger patch table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Diagnostics-coreclr NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants